// ==UserScript==
// @name           KoLMeta
// @namespace      http://localhost
// @description    A typechecker for KoL's combat macro language.
// @include        http://*kingdomofloathing.com/account_combatmacros.php*
// @resource       prototype http://kolmeta.googlecode.com/svn/trunk/src/prototype.js
// @resource       lib http://kolmeta.googlecode.com/svn/trunk/src/lib.js
// @resource       ometa-base http://kolmeta.googlecode.com/svn/trunk/src/ometa-base.js
// @resource       parser http://kolmeta.googlecode.com/svn/trunk/src/parser.js
// @resource       bs-js-compiler http://kolmeta.googlecode.com/svn/trunk/src/bs-js-compiler.js
// @resource       bs-ometa-compiler http://kolmeta.googlecode.com/svn/trunk/src/bs-ometa-compiler.js
// @resource       bs-ometa-optimizer http://kolmeta.googlecode.com/svn/trunk/src/bs-ometa-optimizer.js
// @resource       bs-ometa-js-compiler http://kolmeta.googlecode.com/svn/trunk/src/bs-ometa-js-compiler.js
// @resource       ometa-script-tag http://kolmeta.googlecode.com/svn/trunk/src/ometa-script-tag.js
// ==/UserScript==

// OMeta copyright Alessandro Warth.

var ometaCode = (<r><![CDATA[
/*Weird-ass behavior: mark, goto, sub, and call can all take a newline/semicolon as a label.
                        This functionality is not supported.*/
function isNum(text){
    if(typeof text == "number"){
        return true;
    }
    if(typeof text == "string"){
        for(var i = 0; i < text.length && text[i].isDigit(); i++){}
        return i == text.length;
    }
    return false;        
}
function numericError(name, arg, line){
    return ((isNum(arg[0])) ? [] : 'Line ' + line + ': invalid argument to ' + name + ": " + arg[0] + '; expected a number.');
}
function isItem(text){return true;}
function isSkill(text){return true;}
function isStringConstant(str, line){
    return ((str.indexOf(' ') == -1 && str.indexOf('\t') == -1) || (str.charAt(0) == '"' && str.charAt(str.length - 1) == '"')) ?
      [] : 'Line ' + line + ': invalid string constant.';
}
var functionTable = {}, labelTable = {};
function resetTables(){functionTable = {}; labelTable = {};}
function registerFunction(name, line){
    var errors = [];
    if(functionTable[name] === undefined){
        functionTable[name] = line;
    }
    else{
        errors.push('Line ' + line + ': redefinition of subroutine ' + name + '.');
    }
    if(labelTable[name] !== undefined){
        errors.push('Line ' + line + ': subroutine ' + name + ' shares a name with a mark.');
    }
    return errors;
}
function registerLabel(name, line){
    var errors = [];
    if(labelTable[name] === undefined){
        labelTable[name] = line;
    }
    else{
        errors.push('Line ' + line + ': redefinition of mark ' + name + '.');
    }
    if(functionTable[name] !== undefined){
        errors.push('Line ' + line + ': mark ' + name + ' shares a name with a subroutine.');
    }
    return errors;
}
function checkFunction(name, line){
    return ((functionTable[name] === undefined) ? 'Line ' + line + ': call to a subroutine ' + name + ' that does not exist.' : []);
}
function checkLabel(name, line){
    return ((labelTable[name] === undefined) ? 'Line ' + line + ': goto to a mark ' + name + ' that does not exist.' : []);
}
// First pass to get obvious errors/ build symbol table
ometa SymbolTable {
    dispatch       = [:t apply(t):ans] -> ans,
    attack         = :line -> [],
    pickpocket     = :line -> [],
    summonspirit   = :line -> [],
    runaway        = :line -> [],
    skill          = :line :arg -> ((isSkill(arg))? [] : 'Line ' + line + ': Invalid skill: ' + arg + '.'), //TODO: fuzzy matching
    use            = :line :arg anything?:arg2 -> ((isItem(arg) && isItem(arg2))? [] : //TODO: fuzzy matching
                         'Line ' + line + ': Invalid item: ' + ((isItem(arg)) ? arg2 : arg) + '.'),
    times          = :line :arg -> numericError('times', arg, line),
    hpbelow        = :line :arg -> numericError('hpbelow', arg, line),
    hppercentbelow = :line :arg -> numericError('hppercentbelow', arg, line),
    mpbelow        = :line :arg -> numericError('mpbelow', arg, line),
    mppercentbelow = :line :arg -> numericError('mppercentbelow', arg, line),
    didcritical    = :line :arg -> numericError('didcritical', arg, line),
    beenhit        = :line :arg -> numericError('beenhit', arg, line),
    missed         = :line :arg -> numericError('missed', arg, line),
    familiarattacked = :line :arg -> numericError('familiarattacked', arg, line),
    pastround      = :line :arg -> numericError('pastround', arg, line),
    haseffect      = :line :arg -> [], // TODO: fuzzy matching
    hascombatitem  = :line :arg -> [], // TODO: fuzzy matching
	matcher        = :line :arg -> isStringConstant(arg[0], line),
	monstername    = :line :arg -> isStringConstant(arg[0], line), //TODO: fuzzy matching
    gotjump        = :line -> [],
    if             = :line dispatch:predicate macro:stmtlist -> stmtlist.concat(predicate),
    and            = dispatch:p1 dispatch:p2 -> [p1,p2].flatten(),
    or             = dispatch:p1 dispatch:p2 -> [p1,p2].flatten(),
    not            = dispatch:p,
    while          = :line dispatch:predicate macro:stmtlist -> stmtlist.concat(predicate),
    repeat         = dispatch:stmt (dispatch | anything)*:d -> d.flatten().concat(stmt),
    mark           = :line :label -> registerLabel(label[0], line),
    goto           = :line :label dispatch?:predicate -> ((predicate === undefined) ? [] : predicate),
    abort          = :line :condition -> [], //TODO: predicate goes here?
    sub            = :line :name macro:body -> body.concat(registerFunction(name[0], line)),
    call           = :line :functionname dispatch?:predicate -> ((predicate === undefined) ? [] : predicate),
    macro          = [dispatch*:d] -> d.flatten()
}

ometa Typechecker <: SymbolTable {
    call           = :line :name anything?:predicate -> checkFunction(name[0], line),
    goto           = :line :label anything?:predicate -> checkLabel(label[0], line),
    sub            = :line :name macro:body -> body,
    mark           = :line :label -> [],
    repeat         = dispatch:stmt :rest -> stmt,
    while          = :line :predicate macro:stmtlist -> stmtlist,
    if             = :line :predicate macro:stmtlist -> stmtlist,
    skill          = :line :arg -> [],
    use            = :line :arg anything?:arg2 -> []
}

var errorMessage = "unknown";
var lineNumber = 1;
var warnings = [];
var numParsedLines = 0;
function isDelimiter(character){return character == '\n' || character == ';'}
// The lexer delimits tokens by using spaces, but some tokens should be strings containing spaces.
// This function reconstructs these tokens.
function reconstruct(list){
    var stringConstant = [];
    for(var i = 0; i < list.length; i++){
        stringConstant.push(list[i][0]);
    }
    return [stringConstant.join(" "), list[0][1]];
}
function isValidString(node){
    return '\n;&&||)'.indexOf(node[0]) == -1;
}

function isValidItem(node){
    return node[0] != '\n' && node[0] != ';' && node[0] != ",";
}
// Weird behavior w/ regards to string constants: mark and sub don't mind having multi-word names,
// although only the first word is actually used. goto and call(!) can have predicates after them, 
// so they are restricted to one word labels only.
ometa CombatMacro {
    token :t       = anything:a ?(a[0] == t) -> {lineNumber = a[1]; a},
    // Numeric predicate- takes a number as an argument
    numPredicate   = ("times" | "hpbelow" | "hppercentbelow" | "mpbelow" | "mppercentbelow" | "didcritical"
                   | "beenhit" | "missed" | "familiarattacked" | "pastround"):p 
                         ?(this.err("missing argument for predicate " + p[0]))
                         anything:a ?(!isDelimiter(a[0])) -> {p.push(a);p},
    // String predicate- takes a string constant (may have spaces if enclosed by quotes)
    // For some reason, having a rule named 'match' makes the typechecker fail.
    strPredicate   = ("match" | "monstername"):m ?(this.err("missing string constant")) strConst:s -> 
                        {if(m[0] == 'match'){m[0] = 'matcher';}m.push(s);m}, 
    strConst       = (anything:a ?(isValidString(a)) -> a)+:q -> reconstruct(q),
    itemConst      = (anything:a ?(isValidItem(a)) -> a)+:q -> reconstruct(q),
    lineConst      = (anything:a ?(!isDelimiter(a[0])) -> a)+:q -> q,
    // Other predicates are predicates that can take other types of arguments(i.e. fuzzy matching on ids).
    otherPredicate = ("haseffect" | "hascombatitem"):p ?(this.err("missing argument for predicate " + p[0]))
                         strConst:a -> {p.push(a);p},
    primePredicate =  "gotjump" | otherPredicate | numPredicate | strPredicate | "!" primePredicate:p -> ['not', p] | predicate,
    parenPredicate = "(" predicate:p ?(this.err("missing parenthesis")) ")" -> p | primePredicate,
    predicate      = &anything:a ?(this.err("unrecognized predicate: " + a[0]))
                    (predicate:p1 "||" parenPredicate:p2 -> ['or', p1, p2]
                   | predicate:p1 "&&" parenPredicate:p2 -> ['and', p1, p2]
                   | parenPredicate):p -> p,
    // potential newline errors seem to be obscuring some more descriptive ones.
    // insert to add newline errors back in: &anything:a ?(this.err("missing new line/semicolon"))
    newline        = ("\n" | ";"):n -> {numParsedLines = n[1]; n},
    skill          = "skill":s ?(this.err("missing skill id/name")) strConst:a -> {s.push(a);s},
    item           = "use":u ?(this.err("missing item id/name")) itemConst:item
                         ("," ?(this.err("missing item id/name")) itemConst:item2)? 
                         -> {u.push(item);if(item2 !== undefined){u.push(item2);}u},
    action         = "attack" | "pickpocket" | "summonspirit" | "runaway" | skill | item,
    if             = "if":i predicate:p newline stmtList:x ?(this.err("missing endif")) "endif" -> {i.push(p);i.push(x);i},
    while          = "while":w predicate:p newline stmtList:x ?(this.err("missing endwhile")) "endwhile" -> {w.push(p);w.push(x);w},
    repeat         = newline "repeat" predicate?:p -> ((p === undefined) ? [] : p),
    mark           = "mark":m ?(this.err("missing mark label")) lineConst:label  -> {m.push(label[0]);m},
    goto           = "goto":g anything:label ?(isDelimiter(label[0])? !this.err("missing goto label") : true)
                         predicate?:condition -> {g.push(label);if(condition !== undefined){g.push(condition);}g},
    abort          = "abort":a (predicate | strConst)?:r -> {a.push(r);a},
    sub            = "sub":s lineConst:name newline stmtList:body ?(body.length == 0 ||this.err("missing endsub")) "endsub" ->
                         {s.push(name[0]);s.push(body);s},
    call           = "call":c anything:functionName predicate?:p -> {c.push(functionName);if(p !== undefined){c.push(p);}c},
    stmt           = newline* &anything:b ?(this.err("Invalid command: " + b[0]))
                         ((action | if | while | mark | goto | abort | sub | call):a repeat*:r) newline+ ->
                         {var result = r; if(r.length == 0){result = a;}else{result.unshift(a);result.unshift('repeat');} result},
    icon           = "icon" anything:name,
    stmtList       = newline* stmt*:s -> s,
    macro          = icon? stmt*:s -> s
}

CombatMacro.initialize = function(){
  this.warn = function(message, lineNum){warnings.push({text:message, lineNumber:lineNum}); return true;}
  this.err = function(message){errorMessage = message; return true;}
}

ometa Lexer <: Parser{
    whitespace = ' ' | '\t',
    strchr    = char:c ?('\n; \t'.indexOf(c) == -1) -> c,
    nonnewline = char:c ?(c !== '\n' && c !== ';') -> c,
    newline    = ('\n' | ';'):n -> {if(n == '\n'){this.lineNumber++;} n},
    comment    = '#' nonnewline*:c newline -> [],
    lexeme     = newline:n -> [n, this.lineNumber - ((n == '\n') ? 1 : 0)]
               | comment -> undefined
               | ('!' | "&&" | "||" | '(' | ')' | ','):l -> [l, this.lineNumber]
               | strchr+:c -> [c.join(""), this.lineNumber],
    lexemeList = whitespace* lexeme:l lexemeList:ls -> {if(l !== undefined){ls.push(l);}ls}
               | whitespace* lexeme:l -> [l],
    program    = lexemeList
}

Lexer.initialize = function(){this.lineNumber = 1;}

function handler(m, index){
    return "Parse error near line " + lineNumber + "; possible cause: " + errorMessage + ".";
}

function numLines(tokens){
    var num = 0;
    for(var i = 0; i < tokens.length; i++){
        if(tokens[i][0] == '\n' || tokens[i][0] == ';'){
            num++;
        }
    }
    return num;
}
function typecheck(text){
    resetTables();
	text += ';';
    var tokens = Lexer.matchAll(text, 'program');
    tokens.reverse();
    var ast = CombatMacro.matchAll(tokens, 'macro', undefined, handler);
	var errorText = (numParsedLines < numLines(tokens) || ast.length == 0)?
		"Parse error near line " + lineNumber + "; possible cause: " + errorMessage + ".\n" : '';
    var parentNode = document.getElementById('errors').parentNode;
    parentNode.removeChild(document.getElementById('errors'));
    var errorContainer = document.createElement('div');
    errorContainer.id = 'errors';
	if(errorText == ''){
		var errors = SymbolTable.match(ast, 'macro').concat(Typechecker.match(ast, 'macro'));
		for (var j = 0; j < errors.length; j++){
			errorContainer.appendChild(document.createElement('div'));
            errorContainer.lastChild.textContent = errors[j];
		}
        if(errors.length == 0){
            errorContainer.textContent = 'Everything looks okay.'; 
        }
	}
    else{
        errorContainer.appendChild(document.createElement('div'));
        errorContainer.firstChild.textContent = errorText;
    }
    parentNode.appendChild(errorContainer);
}
]]></r>).toString();

var lastUpdate = GM_getValue('lastUpdate', '');
var lastChecked = GM_getValue('lastChecked', '')
lastUpdate = (lastUpdate == '') ? new Date() : new Date(lastUpdate);
lastChecked = (lastUpdate == '') ? new Date() : new Date(lastChecked);
GM_setValue('lastUpdate', lastUpdate.toString());
if(new Date().getTime() - lastChecked.getTime() > 86400000){// More than one day has passed since the last version check
    GM_setValue('lastChecked', (new Date().getTime()).toString());
    GM_xmlhttpRequest({method:'GET',
                       url:'http://code.google.com/feeds/p/kolmeta/updates/basic', 
                       onload:function(response){
                           var parser = new DOMParser();
                           var doc = parser.parseFromString(response.responseText, 'text/xml');
                           var updateDate = new Date(doc.getElementsByTagName('updated')[0].textContent);
                           if(updateDate > lastUpdate){
                               document.getElementById('errors').textContent = 'New version available at http://kolmeta.googlecode.com/svn/trunk/src/kolmeta.user.js';
                           }                   
                       }});
}
var dependencies = [
'prototype',
'lib',
'ometa-base',
'parser',
'bs-js-compiler',
'bs-ometa-compiler',
'bs-ometa-optimizer',
'bs-ometa-js-compiler',
'ometa-script-tag'
];

for (var i in dependencies) {
    var script = document.createElement('script');
    script.textContent = GM_getResourceText(dependencies[i]);
	script.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(script);
}

function typecheck(){
    var script = document.createElement('script');
    script.textContent =
        "typecheck((<r><![CDATA[" + document.getElementsByTagName('textarea')[0].value + "]]></r>).toString());";
	script.type = 'text/javascript';
    document.getElementsByTagName('head')[0].appendChild(script);
    document.getElementsByTagName('head')[0].removeChild(script);
}

    var ometaLoaded = false;
    function startCheck(evt){
        evt.preventDefault();
        if(!ometaLoaded){
            ometaLoaded = true;
			var script = document.createElement('script');
            var scriptText = 
                "var ometaText = (<r><![CDATA[" + ometaCode + "]]></r>).toString();" +
                "eval(translateCode(ometaText));";
            script.textContent = scriptText;
			script.type = 'text/javascript';
     	    document.getElementsByTagName('head')[0].appendChild(script);
        }
        typecheck();
    }
    var textarea = document.getElementsByTagName('textarea');
    if(textarea.length !== 0){
        var checkButton = document.createElement('input');
        checkButton.value = 'Check Macro';
        checkButton.className = 'button';
        checkButton.type = 'submit';
        checkButton.addEventListener('click', startCheck, true);
        textarea[0].parentNode.appendChild(checkButton);
		var resultText = document.createElement('div');
		resultText.id = 'errors';
        textarea[0].parentNode.appendChild(resultText);
    }
// TODO: fuzzy matching(see SymbolTable for specifics)


